// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2018-2023.
 * Description: irq: merge high-precision timer can reduce interrupts and context switches
 * Author: langfei
 * Create: 2023-12-25
 */

#include <linux/percpu-defs.h>
#include <linux/kernel_stat.h>
#include <linux/percpu.h>

#include <asm/apic.h>
#include <asm/cpufeatures.h>

#include "apic_high_precision.h"

#define LAPIC_CAL_LOOPS	(HZ / 10)
#define APIC_DIVISOR	16
#define TSC_DIVISOR	8

static DEFINE_PER_CPU(unsigned long, deadline_delta);

int deadline_delta_func(unsigned long delta)
{
	int divisor;

	divisor = *this_cpu_ptr(&period_divisor);
	if (divisor != 0) {
		*this_cpu_ptr(&deadline_delta) = delta;
		return 0;
	}
	return -1;
}

static void setup_APIC(unsigned long clocks, unsigned int flags)
{
	unsigned int tmp_value;

	apic_write(APIC_LVTT, flags);
	tmp_value = apic_read(APIC_TDCR);
	apic_write(APIC_TDCR,
		(tmp_value & ~(APIC_TDR_DIV_1 | APIC_TDR_DIV_TMBASE)) |
		APIC_TDR_DIV_16);
	apic_write(APIC_TMICT, clocks);
}

static int calculate_lapic_timer_period(void)
{
	int i;
	u64 tsc_perj = 0, tsc_start = 0;
	unsigned long flags;
	long t1, t2;
	u64 tsc_now;

	if (!tsc_khz)
		return -1;

	local_irq_save(flags);

	/* Setup the APIC counter to maximum. */
	setup_APIC(0xffffffff, LOCAL_TIMER_VECTOR | APIC_LVT_TIMER_PERIODIC | APIC_LVT_MASKED);

	tsc_start = rdtsc();
	tsc_perj = div_u64((u64)tsc_khz * 1000, HZ);
	t1 = apic_read(APIC_TMCCT);

	local_irq_restore(flags);

	for (i = 0; i < LAPIC_CAL_LOOPS; i++) {
		while (1) {
			tsc_now = rdtsc();
			if ((tsc_now - tsc_start) >= tsc_perj) {
				tsc_start += tsc_perj;
				break;
			}
			cpu_relax();
		}
	}

	local_irq_save(flags);
	t2 = apic_read(APIC_TMCCT);
	local_irq_restore(flags);

	lapic_timer_period = (t1 - t2) * APIC_DIVISOR / LAPIC_CAL_LOOPS;

	return 0;
}

void change_to_deadline(void *arg)
{
	u64 tsc;
	unsigned long flags, delta;

	local_irq_save(flags);

	delta = *this_cpu_ptr(&deadline_delta);

	if (!this_cpu_has(X86_FEATURE_TSC_DEADLINE_TIMER)) {
		setup_APIC(delta, LOCAL_TIMER_VECTOR);
	} else {
		setup_APIC(0, LOCAL_TIMER_VECTOR | APIC_LVT_TIMER_TSCDEADLINE);

		asm volatile("mfence" : : : "memory");

		tsc = rdtsc();
		wrmsrl(MSR_IA32_TSC_DEADLINE, tsc + (((u64) delta) * TSC_DIVISOR));
	}

	local_irq_restore(flags);
}

void change_to_period(void *arg)
{
	int d;
	unsigned long flags;

	d = *this_cpu_ptr(&period_divisor);
	if (d == 0)
		return;

	if (lapic_timer_period == 0 && calculate_lapic_timer_period() < 0)
		return;

	local_irq_save(flags);

	/*
	 * Disable TSC_DEADLINE first, prevent the KVM on the host side from
	 * being interrupted by clock interrupts when the APIC timer type is
	 * changed. As a result, the KVM enters an infinite loop and host panic.
	 */
	wrmsrl(MSR_IA32_TSC_DEADLINE, 0);
	asm volatile("mfence" : : : "memory");
	setup_APIC(lapic_timer_period * HZ / MSEC_PER_SEC / d / APIC_DIVISOR,
			LOCAL_TIMER_VECTOR | APIC_LVT_TIMER_PERIODIC);

	local_irq_restore(flags);
}

